tool
class_name DictionaryEditor

var properties: Dictionary
var prop_root: Array

var editor_prop_data: Dictionary = {}

func _get_prop_data(prop_name: String) -> Dictionary:
	if not editor_prop_data.has(prop_name):
		var prop_dict: Dictionary = properties.get(prop_name)
		var prop_types: Array = prop_dict.get("types")

		var types_str: String = ""
		var types_map: Dictionary = {}
		for i in prop_types.size():
			var type: String = prop_types[i].get("type")
			types_str += "," + type
			types_map[type] = i

		editor_prop_data[prop_name] = {
			types_str = types_str,
			types_map = types_map,
			editor_index = -1,
			editor_type = "",
			default_param = {},
		}

	return editor_prop_data[prop_name]

func _get_default_param(prop_name: String, type: String) -> Dictionary:
	var prop_data: Dictionary = _get_prop_data(prop_name)
	var dp_dict: Dictionary = prop_data.get("default_param")

	if not dp_dict.has(type):
		var prop_dict: Dictionary = properties.get(prop_name)
		var prop_types: Array = prop_dict.get("types")

		var default_type: String = prop_dict.get("default_type", "")
		var types_map: Dictionary = prop_data.get("types_map")
		var type_index: int = types_map.get(type, types_map.get(default_type, -1))

		var default_param: Dictionary = {}

		if type_index >= 0 and type_index < prop_types.size():
			for v in prop_types[type_index].get("params"):
				default_param[v[0]] = v[1]

		dp_dict[type] = default_param

	return dp_dict[type]

func _append_prop_list(obj: Object, prop_list: Array, prop_name: String):
	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_types: Array = prop_dict.get("types")
	var prop_data: Dictionary = _get_prop_data(prop_name)

	prop_list.append({
		name = prop_name + "_editor/index",
		type = TYPE_INT,
		usage = PROPERTY_USAGE_EDITOR,
	})
	prop_list.append({
		name = prop_name + "_editor/type",
		type = TYPE_STRING,
		hint = PROPERTY_HINT_ENUM,
		hint_string = prop_data.get("types_str"),
		usage = PROPERTY_USAGE_EDITOR,
	})
	prop_list.append({
		name = prop_name + "_editor/action",
		type = TYPE_STRING,
		hint = PROPERTY_HINT_ENUM,
		hint_string = ",Add,Remove,Duplicate,Change Type",
		usage = PROPERTY_USAGE_EDITOR,
	})

	var editor_index: int = prop_data.get("editor_index", -1)
	if editor_index < 0:
		return

	var props: Array = obj.get(prop_name)
	if props == null or editor_index >= props.size():
		return

	var prop: Dictionary = props[editor_index]
	if prop == null:
		return

	var default_type: String = prop_dict.get("default_type", "")
	var type: String = prop.get("type", default_type)
	var types_map: Dictionary = prop_data.get("types_map")
	var type_index: int = types_map.get(type, types_map.get(default_type, -1))

	if type_index < 0 or type_index >= prop_types.size():
		return

	for v in prop_types[type_index].get("params"):
		var pp: Dictionary = {}
		if v.size() > 2:
			pp = v[2].duplicate(true)
		var sub_name: String = prop_name + "_editor/" + type + "/" + v[0]
		pp["name"] = sub_name
		var sub_type: int = typeof(v[1])
		pp["type"] = sub_type
		pp["usage"] = PROPERTY_USAGE_EDITOR
		prop_list.append(pp)

		if sub_type == TYPE_ARRAY and properties.has(sub_name):
			_append_prop_list(obj, prop_list, sub_name)

func append_prop_list(obj: Object, prop_list: Array):
	for p in prop_root:
		_append_prop_list(obj, prop_list, p)

func get_editor_index(obj: Object, prop_name: String) -> int:
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var props: Array = obj.get(prop_name)
	var editor_index: int = prop_data.get("editor_index", -1)
	editor_index = editor_index if (editor_index >= 0 and editor_index < props.size()) else -1

	return editor_index

func set_editor_index(obj: Object, prop_name: String, index: int, force: bool = false, refresh_only: bool = false) -> void:
	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var props: Array = obj.get(prop_name)
	var editor_index: int = prop_data.get("editor_index", -1)
	if refresh_only:
		index = editor_index
	index = index if (index >= 0 and index < props.size()) else -1

	if force or editor_index != index:
		prop_data["editor_index"] = index
		if index >= 0 and index < props.size():
			prop_data["editor_type"] = props[index].get("type", prop_dict.get("default_type", ""))
		obj.property_list_changed_notify()
		if obj is Spatial:
			obj.update_gizmo()

func get_prop(obj: Object, prop_name: String):
	var pos: int = prop_name.rfind("_editor/")
	if pos < 0:
		return null

	var prop_subname: String = prop_name.substr(pos + 8)
	prop_name = prop_name.left(pos)

	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var pos2: int = prop_subname.find("/")

	if pos2 < 0:
		match prop_subname:
			"index":
				var props: Array = obj.get(prop_name)
				var editor_index: int = prop_data.get("editor_index", -1)
				return editor_index if (editor_index >= 0 and editor_index < props.size()) else -1
			"type":
				return prop_data.get("editor_type", "")
			"action":
				return ""
	else:
		var type: String = prop_subname.left(pos2)
		prop_subname = prop_subname.substr(pos2 + 1)

		var props: Array = obj.get(prop_name)
		var editor_index: int = prop_data.get("editor_index", -1)
		if editor_index >= 0 and editor_index < props.size():
			var prop: Dictionary = props[editor_index]
			var dt: String = prop_dict.get("default_type", "")
			var t: String = prop.get("type", dt)

			var value = null

			if type == t:
				value = prop.get(prop_subname, null)
			else:
				push_warning("Type mismatch: '%s' from property name, '%s' from data" % [type, t])

			if value == null:
				var default_param: Dictionary = _get_default_param(prop_name, type)
				var duplicated: bool = false

				value = default_param.get(prop_subname, null)

				if value is Array or value is Dictionary:
					value = value.duplicate(true)
					duplicated = true
				elif value is Object and value.has_method("duplicate"):
					value = value.duplicate()
					duplicated = true

				if duplicated:
					# need to store it to prop since it is returned as a reference
					prop[prop_subname] = value

			return value

	return null

func set_prop(obj: Object, prop_name: String, prop_value) -> void:
	var pos: int = prop_name.rfind("_editor/")
	if pos < 0:
		return

	var prop_subname: String = prop_name.substr(pos + 8)
	prop_name = prop_name.left(pos)

	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var pos2: int = prop_subname.find("/")

	if pos2 < 0:
		match prop_subname:
			"index":
				set_editor_index(obj, prop_name, prop_value)
			"type":
				prop_data["editor_type"] = prop_value
			"action":
				var props: Array = obj.get(prop_name)
				var editor_index: int = prop_data.get("editor_index", -1)
				editor_index = editor_index if (editor_index >= 0 and editor_index < props.size()) else -1
				match prop_value:
					"Add":
						var editor_type: String = prop_data.get("editor_type", "")
						if editor_type != "":
							var new_index: int = -1
							if obj.has_method("_dict_edit_action_add"):
								new_index = obj._dict_edit_action_add(prop_name, props, editor_index, editor_type)
							else:
								props.append({type = editor_type})
								new_index = props.size() - 1
							if new_index >= 0:
								set_editor_index(obj, prop_name, new_index, true)
					"Remove":
						if editor_index >= 0:
							if obj.has_method("_dict_edit_action_remove"):
								obj._dict_edit_action_remove(prop_name, props, editor_index)
							else:
								props.remove(editor_index)
							set_editor_index(obj, prop_name, editor_index, true)
					"Duplicate":
						if editor_index >= 0:
							var new_index: int = -1
							if obj.has_method("_dict_edit_action_duplicate"):
								new_index = obj._dict_edit_action_duplicate(prop_name, props, editor_index)
							else:
								props.append(props[editor_index].duplicate(true))
								new_index = props.size() - 1
							if new_index >= 0:
								set_editor_index(obj, prop_name, new_index, true)
					_:
						print("TODO: %s" % prop_value)
	else:
		var type: String = prop_subname.left(pos2)
		prop_subname = prop_subname.substr(pos2 + 1)

		var props: Array = obj.get(prop_name)
		var editor_index: int = prop_data.get("editor_index", -1)
		if editor_index >= 0 and editor_index < props.size():
			var prop: Dictionary = props[editor_index]
			var dt: String = prop_dict.get("default_type", "")
			var t: String = prop.get("type", dt)

			if type == t:
				if prop.get(prop_subname, null) != prop_value:
					prop[prop_subname] = prop_value
					if obj.has_method("_dict_edit_prop_changed"):
						obj._dict_edit_prop_changed(prop_name, props, editor_index, type, prop_subname, prop_value)
			else:
				push_warning("Type mismatch: '%s' from property name, '%s' from data" % [type, t])

# prop_subnames == null means get all sub properties
# otherwise it's an Array (or Dictionary, ...) with all names
func get_multiple_props(obj: Object, prop_name: String, prop_subnames = null) -> Dictionary:
	var pos: int = prop_name.rfind("_editor/")
	if pos < 0:
		return {}

	var type: String = prop_name.substr(pos + 8)
	prop_name = prop_name.left(pos)

	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var props: Array = obj.get(prop_name)
	var editor_index: int = prop_data.get("editor_index", -1)
	if editor_index < 0 or editor_index >= props.size():
		return {}

	var prop: Dictionary = props[editor_index]
	var dt: String = prop_dict.get("default_type", "")
	var t: String = prop.get("type", dt)

	if type != t:
		push_warning("Type mismatch: '%s' from property name, '%s' from data" % [type, t])
		return {}

	var default_param: Dictionary = _get_default_param(prop_name, type)
	var ret: Dictionary = {}

	if prop_subnames == null:
		ret = default_param.duplicate()
		for k in prop:
			ret[k] = prop[k]
	else:
		for k in prop_subnames:
			if prop.has(k):
				ret[k] = prop[k]
			elif default_param.has(k):
				ret[k] = default_param[k]

	return ret.duplicate(true)

func set_multiple_props(obj: Object, prop_name: String, prop_values: Dictionary) -> void:
	var pos: int = prop_name.rfind("_editor/")
	if pos < 0:
		return

	var type: String = prop_name.substr(pos + 8)
	prop_name = prop_name.left(pos)

	var prop_dict: Dictionary = properties.get(prop_name)
	var prop_data: Dictionary = _get_prop_data(prop_name)

	var props: Array = obj.get(prop_name)
	var editor_index: int = prop_data.get("editor_index", -1)
	if editor_index < 0 or editor_index >= props.size():
		return

	var prop: Dictionary = props[editor_index]
	var dt: String = prop_dict.get("default_type", "")
	var t: String = prop.get("type", dt)

	if type != t:
		push_warning("Type mismatch: '%s' from property name, '%s' from data" % [type, t])
		return

	var changed: bool = false

	for k in prop_values:
		var new_value = prop_values[k]
		if prop.get(k, null) != new_value:
			prop[k] = new_value
			changed = true

	if changed and obj.has_method("_dict_edit_prop_changed"):
		obj._dict_edit_prop_changed(prop_name, props, editor_index, type, "", null)
